package com.qihetimes.earthangel.service.impl;

import ch.hsr.geohash.BoundingBox;
import ch.hsr.geohash.GeoHash;
import ch.hsr.geohash.WGS84Point;
import ch.hsr.geohash.util.VincentyGeodesy;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.hiwepy.qqwry.spring.boot.ext.IPZone;
import com.github.hiwepy.qqwry.spring.boot.ext.QQWry;
import com.qihetimes.earthangel.common.ApiException;
import com.qihetimes.earthangel.common.GoodsStatus;
import com.qihetimes.earthangel.common.IntegralChangeType;
import com.qihetimes.earthangel.mapper.GoodsMapper;
import com.qihetimes.earthangel.model.*;
import com.qihetimes.earthangel.model.bo.GoodsBO;
import com.qihetimes.earthangel.model.bo.GoodsCommentBO;
import com.qihetimes.earthangel.model.bo.GoodsListBO;
import com.qihetimes.earthangel.model.vo.GoodsCommentVO;
import com.qihetimes.earthangel.model.vo.GoodsVO;
import com.qihetimes.earthangel.model.vo.PageVO;
import com.qihetimes.earthangel.model.vo.UserVO;
import com.qihetimes.earthangel.service.*;
import com.qihetimes.earthangel.util.IpUtil;
import lombok.SneakyThrows;
import org.gavaghan.geodesy.Ellipsoid;
import org.gavaghan.geodesy.GeodeticCalculator;
import org.gavaghan.geodesy.GeodeticCurve;
import org.gavaghan.geodesy.GlobalCoordinates;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 物品表 服务实现类
 * </p>
 *
 * @author f4cklangzi@gmail.com
 * @since 2021-12-11
 */
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {
    
    @Resource
    private IGoodsPhotoService goodsPhotoService;
    
    @Resource
    private IUserService userService;
    
    @Resource
    private IFriendRelationService friendRelationService;
    
    @Resource
    private IGoodsFavoriteService goodsFavoriteService;
    
    @Resource
    private IGoodsCommentService goodsCommentService;
    
    @Resource
    private IGoodsLikeService goodsLikeService;
    
    @Resource
    private IIntegralRecordService integralRecordService;
    
    /**
     * 计算两个经纬度之间距离
     *
     * @param fromLatitude  坐标1纬度
     * @param fromLongitude 坐标1经度
     * @param toLatitude    坐标2纬度
     * @param toLongitude   坐标2经度
     * @return 距离 米
     */
    public static double getDistanceMeter(Double fromLatitude, Double fromLongitude, Double toLatitude,
        Double toLongitude) {
        GlobalCoordinates from = new GlobalCoordinates(fromLatitude, fromLongitude);
        GlobalCoordinates to = new GlobalCoordinates(toLatitude, toLongitude);
        GeodeticCurve geoCurve = new GeodeticCalculator().calculateGeodeticCurve(Ellipsoid.WGS84, from, to);
        return geoCurve.getEllipsoidalDistance();
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public GoodsVO upload(GoodsBO param) {
        List<String> photoUrlList = param.getPhotoUrlList();
        if (photoUrlList == null || photoUrlList.isEmpty()) {
            throw new ApiException("照片不能为空!");
        }
        User loginUser = userService.getLoginUser();
        Goods goods = BeanUtil.toBean(param, Goods.class);
        if (goods.getLatitude() != null && goods.getLongitude() != null) {
            GeoHash geoHash = GeoHash.withCharacterPrecision(goods.getLatitude(), goods.getLongitude(), 12);
            goods.setGeohash(geoHash.toBase32());
        }
        goods.setId(IdUtil.objectId());
        goods.setUserId(loginUser.getId());
        goods.setLikeCount(0);
        goods.setFavoriteCount(0);
        goods.setCommentCount(0);
        goods.setStatus(GoodsStatus.SHOWING.name());
        goods.setCreatedAt(LocalDateTime.now());
        // 如果没有传城市则获取IP定位城市
        setGoodsCity(goods);
        save(goods);
        goodsPhotoService.setGoodsPhotos(goods.getId(), photoUrlList);
        integralRecordService.changeIntegralByGoods(IntegralChangeType.UPLOAD_GOODS, goods);
        return buildGoodsVO(photoUrlList, loginUser, goods);
    }
    
    private GoodsVO buildGoodsVO(List<String> photoUrlList, User loginUser, Goods goods) {
        GoodsVO goodsInfo = BeanUtil.toBean(goods, GoodsVO.class);
        goodsInfo.setDistance(0D);
        goodsInfo.setPhotoUrlList(photoUrlList);
        UserVO userVO = BeanUtil.toBean(loginUser, UserVO.class);
        userVO.setFriendFlag(true);
        goodsInfo.setUser(userVO);
        return goodsInfo;
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public GoodsVO update(GoodsBO param) {
        Goods oldGood = getById(param.getId());
        checkGoods(oldGood);
        User loginUser = userService.getLoginUser();
        if (!oldGood.getUserId().equals(loginUser.getId())) {
            throw new ApiException(HttpStatus.FORBIDDEN, "非法操作!");
        }
        if (oldGood.getStatus().equals(GoodsStatus.DELIVERED.getShowName())) {
            throw new ApiException("物品已送出!");
        }
        List<String> photoUrlList = param.getPhotoUrlList();
        if (photoUrlList != null && photoUrlList.isEmpty()) {
            throw new ApiException("照片不能为空!");
        }
        CopyOptions copyOptions = CopyOptions.create();
        copyOptions.ignoreNullValue();
        BeanUtil.copyProperties(param, oldGood, copyOptions);
        if (oldGood.getLatitude() != null && oldGood.getLongitude() != null) {
            GeoHash geoHash = GeoHash.withCharacterPrecision(oldGood.getLatitude(), oldGood.getLongitude(), 12);
            oldGood.setGeohash(geoHash.toBase32());
        }
        setGoodsCity(oldGood);
        oldGood.setUpdatedAt(LocalDateTime.now());
        updateById(oldGood);
        if (photoUrlList != null) {
            goodsPhotoService.setGoodsPhotos(oldGood.getId(), photoUrlList);
        }
        return buildGoodsVO(photoUrlList, loginUser, oldGood);
    }
    
    @SneakyThrows
    private void setGoodsCity(Goods goods) {
        if (goods.getCity() == null || goods.getCity().isEmpty()) {
            String ipAddr = IpUtil.getClientIpAddr();
            IPZone ip = new QQWry().findIP(ipAddr);
            goods.setCity(ip.getMainInfo());
        }
    }
    
    private void checkGoods(Goods goods) {
        if (goods == null) {
            throw new ApiException(HttpStatus.NOT_FOUND, "物品不存在!");
        }
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(String goodsId) {
        Goods goods = getById(goodsId);
        checkGoods(goods);
        User loginUser = userService.getLoginUser();
        if (!goods.getUserId().equals(loginUser.getId())) {
            throw new ApiException(HttpStatus.FORBIDDEN, "非法操作!");
        }
        removeById(goodsId);
        goodsPhotoService.lambdaUpdate().eq(GoodsPhoto::getGoodsId, goodsId).remove();
        goodsFavoriteService.lambdaUpdate().eq(GoodsFavorite::getGoodsId, goodsId).remove();
        goodsCommentService.lambdaUpdate().eq(GoodsComment::getGoodsId, goodsId).remove();
        goodsLikeService.lambdaUpdate().eq(GoodsLike::getGoodsId, goodsId).remove();
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void like(String goodsId) {
        Goods goods = getById(goodsId);
        checkGoods(goods);
        User loginUser = userService.getLoginUser();
        GoodsLike exist = goodsLikeService.lambdaQuery().eq(GoodsLike::getGoodsId, goodsId)
            .eq(GoodsLike::getUserId, loginUser.getId()).one();
        if (exist != null) {
            goodsLikeService.removeById(exist.getId());
            lambdaUpdate()
                .eq(Goods::getId, goodsId)
                .set(Goods::getLikeCount, goods.getLikeCount() - 1)
                .update();
            return;
        }
        GoodsLike like = new GoodsLike();
        like.setId(IdUtil.objectId());
        like.setUserId(loginUser.getId());
        like.setNickName(loginUser.getNickName());
        like.setAvatarUrl(loginUser.getAvatarUrl());
        like.setGoodsId(goodsId);
        like.setCreatedAt(LocalDateTime.now());
        goodsLikeService.save(like);
        lambdaUpdate()
            .eq(Goods::getId, goodsId)
            .set(Goods::getLikeCount, goods.getLikeCount() + 1)
            .update();
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public GoodsComment comment(GoodsCommentBO param) {
        if (param.getCommentId() != null) {
            GoodsComment replyComment = goodsCommentService.getById(param.getCommentId());
            if (replyComment == null) {
                throw new ApiException("回复的评论不存在!");
            }
            if (replyComment.getPid() != null) {
                throw new ApiException("不允许回复评论的回复!");
            }
            param.setGoodsId(replyComment.getGoodsId());
        }
        Goods goods = getById(param.getGoodsId());
        checkGoods(goods);
        User loginUser = userService.getLoginUser();
        GoodsComment comment = new GoodsComment();
        comment.setId(IdUtil.objectId());
        comment.setUserId(loginUser.getId());
        comment.setNickName(loginUser.getNickName());
        comment.setAvatarUrl(loginUser.getAvatarUrl());
        comment.setGoodsId(param.getGoodsId());
        comment.setPid(param.getCommentId());
        comment.setContent(param.getContent());
        comment.setCreatedAt(LocalDateTime.now());
        goodsCommentService.save(comment);
        lambdaUpdate()
            .eq(Goods::getId, param.getGoodsId())
            .set(Goods::getCommentCount, goods.getCommentCount() + 1)
            .update();
        return comment;
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void favorite(String goodsId) {
        Goods goods = getById(goodsId);
        checkGoods(goods);
        User loginUser = userService.getLoginUser();
        GoodsFavorite exist = goodsFavoriteService.lambdaQuery()
            .eq(GoodsFavorite::getGoodsId, goodsId)
            .eq(GoodsFavorite::getUserId, loginUser.getId())
            .one();
        if (exist != null) {
            goodsFavoriteService.removeById(exist.getId());
            lambdaUpdate()
                .eq(Goods::getId, goodsId)
                .set(Goods::getFavoriteCount, goods.getFavoriteCount() - 1)
                .update();
            return;
        }
        GoodsFavorite favorite = new GoodsFavorite();
        favorite.setId(IdUtil.objectId());
        favorite.setUserId(loginUser.getId());
        favorite.setNickName(loginUser.getNickName());
        favorite.setAvatarUrl(loginUser.getAvatarUrl());
        favorite.setGoodsId(goodsId);
        favorite.setCreatedAt(LocalDateTime.now());
        goodsFavoriteService.save(favorite);
        lambdaUpdate()
            .eq(Goods::getId, goodsId)
            .set(Goods::getFavoriteCount, goods.getFavoriteCount() + 1)
            .update();
    }
    
    @Override
    public void send(String goodsId, String userId) {
        Goods goods = getById(goodsId);
        checkGoods(goods);
        User loginUser = userService.getLoginUser();
        if (!goods.getUserId().equals(loginUser.getId())) {
            throw new ApiException(HttpStatus.FORBIDDEN, "非法操作!");
        }
        goods.setStatus(GoodsStatus.DELIVERED.name());
        goods.setToUserId(userId);
        updateById(goods);
        integralRecordService.changeIntegralByGoods(IntegralChangeType.SEND_GOODS, goods);
        integralRecordService.changeIntegralByGoods(IntegralChangeType.GET_GOODS, goods);
    }
    
    @Override
    public void share(String goodsId) {
        Goods goods = getById(goodsId);
        integralRecordService.changeIntegralByGoods(IntegralChangeType.SHARE_GOODS, goods);
    }
    
    @Override
    public PageVO<GoodsVO> list(GoodsListBO param) {
        User loginUser = userService.getLoginUser();
        Double lat = param.getLatitude();
        Double lon = param.getLongitude();
        String city = param.getCity();
        if (lat == null || lon == null) {
            lat = loginUser.getLatitude();
            lon = loginUser.getLongitude();
        }
        if (city == null) {
            city = loginUser.getCity();
        }
        Page<Goods> queryPage = new Page<>(param.getPageNum(), param.getPageSize());
        LambdaQueryChainWrapper<Goods> query = lambdaQuery().orderByDesc(Goods::getCreatedAt);
        if (param.getUserId() != null) {
            query.eq(Goods::getUserId, param.getUserId());
        }
        if (param.getKeyword() != null && !param.getKeyword().isEmpty()) {
            String keyword = param.getKeyword();
            query.and(wrapper -> {
                wrapper.like(Goods::getName, keyword)
                    .or()
                    .like(Goods::getDescription, keyword)
                    .or()
                    .like(Goods::getKeyword, keyword)
                    .or()
                    .like(Goods::getCategory, keyword);
            });
        }
        GoodsStatus searchStatus = GoodsStatus.SHOWING;
        if (param.getSearchType() != null) {
            switch (param.getSearchType()) {
                case LOCATION:
                    if (lat != null && lon != null && param.getCityCondition() == null && param.getDistance() == null) {
                        param.setDistance(5D);
                    } else {
                        param.setCityCondition(true);
                    }
                    break;
                case FRIEND:
                    PageVO<UserVO> friendPage = friendRelationService.getFriendList(loginUser.getId(), 1L,
                        Long.MAX_VALUE);
                    List<String> friendIdList = friendPage.getRecords().stream().map(UserVO::getId)
                        .collect(Collectors.toList());
                    query.in(Goods::getUserId, friendIdList);
                    break;
                case OWNER:
                    searchStatus = null;
                    query.eq(Goods::getUserId, loginUser.getId());
                    break;
                case FAVORITE:
                    searchStatus = null;
                    List<String> goodsIdList = goodsFavoriteService.lambdaQuery()
                        .eq(GoodsFavorite::getUserId, loginUser.getId()).list()
                        .stream().map(GoodsFavorite::getGoodsId).collect(Collectors.toList());
                    query.in(Goods::getId, goodsIdList);
                    break;
                case GET:
                    query.eq(Goods::getToUserId, loginUser.getId());
                    searchStatus = GoodsStatus.DELIVERED;
                    break;
                default:
            }
        }
        if (param.getDistance() != null && param.getDistance() > 0) {
            applyDistanceCondition(lat, lon, query, param.getDistance());
        }
        if (param.getCityCondition() != null && param.getCityCondition()) {
            query.like(Goods::getCity, city);
        }
        if (param.getGoodsIdList() != null && !param.getGoodsIdList().isEmpty()) {
            searchStatus = null;
            query.in(Goods::getId, param.getGoodsIdList());
        }
        if (searchStatus != null) {
            query.eq(Goods::getStatus, searchStatus);
        }
        query.page(queryPage);
        PageVO<GoodsVO> resPage = new PageVO<>(queryPage);
        if (queryPage.getRecords().isEmpty()) {
            return resPage;
        }
        List<String> goodsIdList = new ArrayList<>();
        List<String> userIdList = new ArrayList<>();
        queryPage.getRecords().forEach(item -> {
            goodsIdList.add(item.getId());
            userIdList.add(item.getUserId());
        });
        Map<String, List<String>> photoMap = goodsPhotoService.lambdaQuery()
            .in(GoodsPhoto::getGoodsId, goodsIdList).list()
            .stream().collect(Collectors.groupingBy(GoodsPhoto::getGoodsId,
                Collectors.mapping(GoodsPhoto::getPhotoUrl, Collectors.toList())));
        Map<String, Set<String>> goodsLikeUserSetMap = goodsLikeService.lambdaQuery()
            .in(GoodsLike::getGoodsId, goodsIdList).list()
            .stream().collect(Collectors.groupingBy(GoodsLike::getGoodsId,
                Collectors.mapping(GoodsLike::getUserId, Collectors.toSet())));
        Map<String, Set<String>> goodsFavoriteUserSetMap = goodsFavoriteService.lambdaQuery()
            .in(GoodsFavorite::getGoodsId, goodsIdList).list()
            .stream().collect(Collectors.groupingBy(GoodsFavorite::getGoodsId,
                Collectors.mapping(GoodsFavorite::getUserId, Collectors.toSet())));
        List<GoodsComment> goodsCommentList = goodsCommentService.lambdaQuery()
            .orderByDesc(GoodsComment::getCreatedAt)
            .in(GoodsComment::getGoodsId, goodsIdList).list();
        Map<String, GoodsCommentVO> goodsCommentIdMap = goodsCommentList.stream()
            .collect(Collectors.toMap(GoodsComment::getId, value -> BeanUtil.toBean(value, GoodsCommentVO.class)));
        Map<String, List<GoodsCommentVO>> goodsCommentListMap = goodsCommentList.stream().map(goodsComment -> {
            GoodsCommentVO commentVO = goodsCommentIdMap.get(goodsComment.getId());
            if (commentVO.getReplyList() == null) {
                commentVO.setReplyList(new ArrayList<>());
            }
            if (commentVO.getPid() != null) {
                GoodsCommentVO parentComment = goodsCommentIdMap.get(goodsComment.getPid());
                List<GoodsCommentVO> replyList = parentComment.getReplyList();
                if (replyList == null) {
                    replyList = new ArrayList<>();
                }
                replyList.add(commentVO);
                parentComment.setReplyList(replyList);
            }
            return commentVO;
        })
            .filter(goodsCommentVO -> goodsCommentVO.getPid() == null)
            .collect(Collectors.groupingBy(GoodsComment::getGoodsId));
        String userId = loginUser.getId();
        PageVO<UserVO> friendPage = friendRelationService.getFriendList(userId, 1L, Long.MAX_VALUE);
        Set<String> friendSet = friendPage.getRecords().stream().map(UserVO::getId).collect(Collectors.toSet());
        Map<String, UserVO> friendMap = userService.lambdaQuery().in(User::getId, userIdList).list()
            .stream().map(user -> {
                UserVO userVO = BeanUtil.toBean(user, UserVO.class);
                userVO.setFriendFlag(user.getId().equals(userId) || friendSet.contains(user.getId()));
                return userVO;
            }).collect(Collectors.toMap(UserVO::getId, value -> value));
        List<GoodsVO> goodsVOList = new ArrayList<>();
        for (Goods goods : queryPage.getRecords()) {
            GoodsVO goodsVO = BeanUtil.toBean(goods, GoodsVO.class);
            goodsVO.setPhotoUrlList(photoMap.getOrDefault(goodsVO.getId(), Collections.emptyList()));
            goodsVO.setUser(friendMap.get(goodsVO.getUserId()));
            if (lat != null && lon != null && goods.getLatitude() != null && goods.getLongitude() != null) {
                double distance = getDistanceMeter(lat, lon, goods.getLatitude(), goods.getLongitude());
                goodsVO.setDistance(distance);
            } else {
                goodsVO.setDistance(null);
            }
            boolean likeFlag = goodsLikeUserSetMap.getOrDefault(goods.getId(), new HashSet<>()).contains(userId);
            goodsVO.setLikeFlag(likeFlag);
            boolean favoriteFlag = goodsFavoriteUserSetMap.getOrDefault(goods.getId(), new HashSet<>())
                .contains(userId);
            goodsVO.setFavoriteFlag(favoriteFlag);
            List<GoodsCommentVO> commentList = goodsCommentListMap.getOrDefault(goods.getId(), new ArrayList<>());
            goodsVO.setCommentList(commentList);
            goodsVOList.add(goodsVO);
        }
        resPage.setRecords(goodsVOList);
        return resPage;
    }
    
    private void applyDistanceCondition(Double lat, Double lon, LambdaQueryChainWrapper<Goods> query, Double distance) {
        WGS84Point center = new WGS84Point(lat, lon);
        double radius = distance * 1000;
        WGS84Point northEast = VincentyGeodesy.moveInDirection(
            VincentyGeodesy.moveInDirection(center, 0.0D, radius), 90.0D, radius
        );
        WGS84Point southWest = VincentyGeodesy.moveInDirection(
            VincentyGeodesy.moveInDirection(center, 180.0D, radius), 270.0D, radius
        );
        BoundingBox boundingBox = new BoundingBox(northEast, southWest);
        query.between(Goods::getLatitude, boundingBox.getMinLat(), boundingBox.getMaxLat())
            .between(Goods::getLongitude, boundingBox.getMinLon(), boundingBox.getMaxLon());
    }
}
